package hu.mapro.model.generator;

import hu.mapro.model.Getter;
import hu.mapro.model.Setter;
import hu.mapro.model.domain.BeanInformation;
import hu.mapro.model.impl.BaseField;
import hu.mapro.model.impl.BaseReadWriteField;
import hu.mapro.model.impl.BaseReadableField;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.sun.codemodel.ClassType;
import com.sun.codemodel.CodeWriter;
import com.sun.codemodel.JArray;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;

public class ModelGenerator {

	JCodeModel cm;
	
	public ModelGenerator(JCodeModel cm) {
		super();
		this.cm = cm;
	}

	public void generateFields(
			final OutputStream out,
			String packageName,
			String className,
			Annotater annotater,
			JClass serviceClass,
			BeanInformation... beanInfos
	) {
		doGenerate(packageName, className, annotater, beanInfos);

		CodeWriter writer = new CodeWriter() {
			
			@Override
			public OutputStream openBinary(JPackage pkg, String fileName)
					throws IOException {
				return out;
			}
			
			@Override
			public void close() throws IOException {
				out.close();
			}
		};

		try {
			cm.build(writer);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		
	}

	public void generateFields(
			File destDirectory,
			String packageName,
			String className,
			Annotater annotater,
			BeanInformation... beanInfos
	) {
		doGenerate(packageName, className, annotater, beanInfos);
		
		
		try {
			cm.build(destDirectory);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		
	}
	
	
	class Generation {
		private void doGenerate(String packageName, String className,
				Annotater annotater, BeanInformation... beanInfos) throws JClassAlreadyExistsException {
				JDefinedClass dc = cm._class(
						JMod.PUBLIC|JMod.FINAL, 
						packageName+"."+className,
						ClassType.CLASS
				);

				if (annotater!=null) {
					annotater.annotate(dc);
				}
				
				for (BeanInformation bi : beanInfos) {
					
					JClass beanClass = bi.getBeanClass();

					JDefinedClass beanFieldsClass = dc._class(
							JMod.STATIC|JMod.PUBLIC|JMod.FINAL, 
							beanClass.name()
//							StringUtils.uncapitalize(beanClass.name())
					);

					
					
					JClass baseClass = cm.ref(BaseField.class);
					JClass readWriteBaseClass = cm.ref(BaseReadWriteField.class);
					JClass baseClassNarrowed = baseClass.narrow(beanClass);
					JClass readOnlyBaseClass = cm.ref(BaseReadableField.class);
					
					JArray allArray = JExpr.newArray(baseClass);
					JArray readOnlyArray = JExpr.newArray(readOnlyBaseClass);
					JArray readWriteArray = JExpr.newArray(readWriteBaseClass);
					
					for (PropertyInformation pd : bi.getProperties()) {
					
						boolean readable = pd.getReadMethod()!=null;
						boolean writable = pd.getWriteMethod()!=null;
						
						JDefinedClass impl = dc._class(
								JMod.STATIC|JMod.PUBLIC|JMod.FINAL,
								"_"+beanClass.name()+"_"+pd.getName()
						);
						
						JMethod c = impl.constructor(JMod.PUBLIC);
						c.body().invoke("setPropertyName").arg(JExpr.lit(pd.getName()));
						c.body().invoke("setLabel").arg(JExpr.lit(getReadableSymbolName(pd.getName())));
						
						if (readable) {
							impl._implements(cm.ref(Getter.class).narrow(beanClass, pd.getType()));
							
							JMethod getter = impl.method(JMod.PUBLIC|JMod.FINAL, pd.getType(), "get");
							getter.param(beanClass, "object");
							getter.body()._if(JExpr.ref("object").eq(JExpr._null()))._then()._return(JExpr._null());
							JInvocation ret = JExpr.invoke(JExpr.ref("object"), pd.getReadMethod());
							getter.body()._return(ret);
							
							c.body().invoke("setHasGetter").arg(JExpr.TRUE);
						}

						if (writable) {
							impl._implements(cm.ref(Setter.class).narrow(beanClass, pd.getType()));
							
							JMethod setter = impl.method(JMod.PUBLIC|JMod.FINAL, cm.VOID, "set");
							setter.param(beanClass, "object");
							setter.param(pd.getType(), "value");
							JInvocation ret = JExpr.invoke(JExpr.ref("object"), pd.getWriteMethod());
							ret.arg(JExpr.ref("value"));
							setter.body().add(ret);
							
							c.body().invoke("setHasSetter").arg(JExpr.TRUE);
						}
						
						
						JFieldVar propertyField = beanFieldsClass.field(
								JMod.STATIC|JMod.PUBLIC|JMod.FINAL, 
								impl, 
								pd.getName(), 
								JExpr._new(impl)
						);
						
						allArray.add(propertyField);
						
						if (readable&&writable) {
							impl._extends(readWriteBaseClass.narrow(beanClass, pd.getType()));
							readWriteArray.add(propertyField);
						} else if (readable&&!writable) {
							impl._extends(readOnlyBaseClass.narrow(beanClass, pd.getType()));
							readOnlyArray.add(propertyField);
						} else {
							impl._extends(baseClassNarrowed);
						}
						
					}
					
					JFieldVar allField = beanFieldsClass.field(
							JMod.STATIC|JMod.PUBLIC|JMod.FINAL, 
							baseClassNarrowed.array(), 
							"_all", 
							allArray
					);
					allField.annotate(SuppressWarnings.class).param("value", "unchecked");
					
					JFieldVar allReadWriteField = beanFieldsClass.field(
							JMod.STATIC|JMod.PUBLIC|JMod.FINAL, 
							readWriteBaseClass.narrow(beanClass, cm.ref(Object.class).wildcard()).array(), 
							"_readWrite", 
							readWriteArray
							);
					allReadWriteField.annotate(SuppressWarnings.class).param("value", "unchecked");
					
					JFieldVar allReadOnlyField = beanFieldsClass.field(
							JMod.STATIC|JMod.PUBLIC|JMod.FINAL, 
							readOnlyBaseClass.narrow(beanClass, cm.ref(Object.class).wildcard()).array(), 
							"_readOnly", 
							readOnlyArray
							);
					allReadOnlyField.annotate(SuppressWarnings.class).param("value", "unchecked");
					

//					JDefinedClass classImpl = dc._class(
//							JMod.STATIC|JMod.PUBLIC|JMod.FINAL,
//							"_"+beanClass.name()
//					);
//					classImpl._implements(cm.ref(Supplier.class).narrow(beanClass));
//					classImpl.
//					
//					
//					
//					
//					supplierField = beanFieldsClass.field(
//							JMod.STATIC|JMod.PUBLIC|JMod.FINAL, 
//							readOnlyBaseClass.narrow(beanClass, cm.ref(Object.class).wildcard()).array(), 
//							"_factory", 
//							readOnlyArray
//					);
					
				}
				
				
		}
		
	}
	
	
	private void doGenerate(String packageName, String className,
			Annotater annotater, BeanInformation... beanInfos) {
		try {
			
			new Generation().doGenerate(packageName, className, annotater, beanInfos);
			
		} catch (JClassAlreadyExistsException e) {
			throw new RuntimeException(e);
		}
	}

    public static String getReadableSymbolName(final String camelCase) {
        final Pattern p = Pattern.compile("[A-Z][^A-Z]*");
        final Matcher m = p.matcher(capitalize(camelCase));
        final StringBuilder builder = new StringBuilder();
        while (m.find()) {
            builder.append(m.group()).append(" ");
        }
        return builder.toString().trim();
    }
    
    public static String capitalize(String str) {
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return str;
        }
        return new StringBuilder(strLen)
            .append(Character.toTitleCase(str.charAt(0)))
            .append(str.substring(1))
            .toString();
    }
	
}
